﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Automation;
using System.Threading;

namespace InnovTech.Wpf.Automation
{
    /// <summary>
    /// A utility class to make it easy to use the ui automation of WPF.
    /// </summary>
    public class UIAutomaton
    {
        #region fields
        /// <summary>
        /// Represents the name of a window which contains UIs to automate.
        /// </summary>
        private string _windowsTitle;
        /// <summary>
        /// Represents the <see cref="AutomationElement"/> instance of the Main-Window which contains UIs to automate.
        /// </summary>
        private AutomationElement _windowElement;

        #endregion fields

        #region Properties
        /// <summary>
        /// Gets the <see cref="AutomationElement"/>of the current window which is the container for the UI to test.
        /// </summary>
        private AutomationElement CurrentWindowElement
        {
            get
            {
                if(_windowElement == null)
                    _windowElement = AutomationElement.RootElement.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, _windowsTitle));
                return _windowElement;
            }
        }

        #endregion Properties

        #region ctor
        public UIAutomaton(string windowTitle)
        {
            _windowsTitle = windowTitle;
        }
        #endregion ctor

        #region methods

        #region private
        /// <summary>
        /// Gets a <see cref="AutomationElement"/> of a <see cref="FrameworkElement"/> whose property <see cref="FrameworkElement.Name"/> has the given value.
        /// </summary>
        /// <param name="frameworkElementName"></param>
        /// <returns></returns>
        private AutomationElement GetElementById(string frameworkElementName)
        {
            AutomationElement element =
                CurrentWindowElement.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.AutomationIdProperty, frameworkElementName));
            return element;
        }

        private static AutomationPattern GetSpecifiedPattern(AutomationElement element, string patternName)
        {
            AutomationPattern[] supportedPattern = element.GetSupportedPatterns();

            foreach (AutomationPattern pattern in supportedPattern)
            {
                if (pattern.ProgrammaticName == patternName)
                    return pattern;
            }

            return null;
        }

        /// <summary>
        /// Insert the given text to the given TextBox or TextBlock
        /// </summary>
        /// <param name="element"></param>
        /// <param name="value"></param>
        private void ProcessTextInputInternal(AutomationElement element,
                                    string value)
        {
            try
            {
                // Validate arguments / initial setup
                if (value == null)
                    throw new ArgumentNullException(
                        "String parameter must not be null.");

                if (element == null)
                    throw new ArgumentNullException(
                        "AutomationElement parameter must not be null");

                // A series of basic checks prior to attempting an insertion.
                //
                // Check #1: Is control enabled?
                // An alternative to testing for static or read-only controls 
                // is to filter using 
                // PropertyCondition(AutomationElement.IsEnabledProperty, true) 
                // and exclude all read-only text controls from the collection.
                if (!element.Current.IsEnabled)
                {
                    throw new InvalidOperationException(
                        "The control with an AutomationID of "
                        + element.Current.AutomationId.ToString()
                        + " is not enabled.\n\n");
                }

                // Check #2: Are there styles that prohibit us 
                //           from sending text to this control?
                if (!element.Current.IsKeyboardFocusable)
                {
                    throw new InvalidOperationException(
                        "The control with an AutomationID of "
                        + element.Current.AutomationId.ToString()
                        + "is read-only.\n\n");
                }


                // Once you have an instance of an AutomationElement,  
                // check if it supports the ValuePattern pattern.
                object valuePattern = null;

                // Control does not support the ValuePattern pattern 
                // so use keyboard input to insert content.
                //
                // NOTE: Elements that support TextPattern 
                //       do not support ValuePattern and TextPattern
                //       does not support setting the text of 
                //       multi-line edit or document controls.
                //       For this reason, text input must be simulated
                //       using one of the following methods.
                //       
                if (!element.TryGetCurrentPattern(
                    ValuePattern.Pattern, out valuePattern))
                {
                    //feedbackText.Append("The control with an AutomationID of ")
                    //    .Append(element.Current.AutomationId.ToString())
                    //    .Append(" does not support ValuePattern.")
                    //    .AppendLine(" Using keyboard input.\n");

                    //// Set focus for input functionality and begin.
                    //element.SetFocus();

                    //// Pause before sending keyboard input.
                    //Thread.Sleep(100);

                    //// Delete existing content in the control and insert new content.
                    //SendKeys.SendWait("^{HOME}");   // Move to start of control
                    //SendKeys.SendWait("^+{END}");   // Select everything
                    //SendKeys.SendWait("{DEL}");     // Delete selection
                    //SendKeys.SendWait(value);
                }
                // Control supports the ValuePattern pattern so we can 
                // use the SetValue method to insert content.
                else
                {
                    //feedbackText.Append("The control with an AutomationID of ")
                    //    .Append(element.Current.AutomationId.ToString())
                    //    .Append((" supports ValuePattern."))
                    //    .AppendLine(" Using ValuePattern.SetValue().\n");

                    // Set focus for input functionality and begin.
                    element.SetFocus();

                    ((ValuePattern)valuePattern).SetValue(value);
                }
            }
            catch (ArgumentNullException exc)
            {
                //feedbackText.Append(exc.Message);
            }
            catch (InvalidOperationException exc)
            {
                //feedbackText.Append(exc.Message);
            }
            finally
            {
                //Feedback(feedbackText.ToString());
            }
        }


        #endregion private

        #region public

        /// <summary>
        /// Processes a Click operation on a <see cref="Button"/> which has the given id.
        /// </summary>
        /// <param name="buttonId"></param>
        public void ProcessButtonClick(string buttonId)
        {
            AutomationElement btnElement = GetElementById(buttonId);
            InvokePattern btnPattern = btnElement.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
            btnPattern.Invoke();

            Thread.Sleep(1500);
        }

        /// <summary>
        /// Processes a Selection operation on a <see cref="ComboBox"/> which has the given id.
        /// </summary>
        /// <param name="comboBoxId"></param>
        /// <param name="itemText">The displayed text of a <see cref="ComboBoxItem"/> to select</param>
        public void ProcessComboBoxSelection(string comboBoxId, String itemText)
        {
            AutomationElement comboBox = GetElementById(comboBoxId);
            if ((comboBox == null) || (itemText == ""))
            {
                throw new ArgumentException(
                    "Argument cannot be null or empty.");
            }

            AutomationPattern automationPatternFromElement = GetSpecifiedPattern(comboBox, "ExpandCollapsePatternIdentifiers.Pattern");
            ExpandCollapsePattern expandCollapsePattern = comboBox.GetCurrentPattern(automationPatternFromElement) as ExpandCollapsePattern;
            expandCollapsePattern.Expand();
            expandCollapsePattern.Collapse();

            AutomationElement listItem = comboBox.FindFirst(TreeScope.Subtree, new PropertyCondition(AutomationElement.NameProperty, itemText));
            automationPatternFromElement = GetSpecifiedPattern(listItem, "SelectionItemPatternIdentifiers.Pattern");
            SelectionItemPattern selectionItemPattern = listItem.GetCurrentPattern(automationPatternFromElement) as SelectionItemPattern;
            selectionItemPattern.Select();

            Thread.Sleep(1500);
        }

        /// <summary>
        /// Processes a Input operation with the given Text on a <see cref="TextBox"/> which has the given id.
        /// </summary>
        /// <param name="id"></param>
        /// <param name="text"></param>
        public void ProcessTextInput(string id, string text)
        {
            AutomationElement textElement = GetElementById(id);
            ProcessTextInputInternal(textElement, text);

            Thread.Sleep(1500);
        }

        /// <summary>
        /// Processes the Selection operation on a <see cref="RadioButton"/> which has the given id.
        /// </summary>
        /// <param name="id"></param>
        public void ProcessRadioButtonSelection(string id)
        {
            AutomationElement element = GetElementById(id);
            if ((element == null))
            {
                throw new ArgumentException(
                    "Argument cannot be null or empty.");
            }
            SelectionItemPattern pattern = element.GetCurrentPattern(SelectionItemPattern.Pattern) as SelectionItemPattern;
            pattern.Select();
            Thread.Sleep(1500);
        }

        /// <summary>
        /// Processes the selection of a <see cref="Selector"/>. 
        /// </summary>
        /// <param name="selectorId">The value of the property <see cref="FrameworkElement.Name"/>of the <see cref="Selector"/> to test.</param>
        /// <param name="itemText">The Text that is shown currently by the <see cref="Selector"/>. 
        /// It can be the 
        /// 1. <see cref="ListBox.SelectedItem"/>, 
        /// 2. <see cref="ComboBox.SelectedItem"/> or 
        /// 3. <see cref="TabControl.SelectedItem.Header"/></param>
        public void ProcessSelection(string selectorId, string itemText)
        {
            AutomationElement selector = GetElementById(selectorId);
            if ((selector == null) || (itemText == ""))
            {
                throw new ArgumentException(
                    "Argument cannot be null or empty.");
            }

            if (selector.Current.ControlType.Equals(ControlType.ComboBox))
            {
                AutomationPattern pattern = GetSpecifiedPattern(selector, "ExpandCollapsePatternIdentifiers.Pattern");
                ExpandCollapsePattern expandCollapsePattern = selector.GetCurrentPattern(pattern) as ExpandCollapsePattern;
                expandCollapsePattern.Expand();
                expandCollapsePattern.Collapse();
            }

            AutomationElement listItem = selector.FindFirst(TreeScope.Subtree, new PropertyCondition(AutomationElement.NameProperty, itemText));
            AutomationPattern automationPatternFromElement = GetSpecifiedPattern(listItem, "SelectionItemPatternIdentifiers.Pattern");
            SelectionItemPattern selectionItemPattern = listItem.GetCurrentPattern(automationPatternFromElement) as SelectionItemPattern;
            selectionItemPattern.Select();

            Thread.Sleep(1500);
        }

        /// <summary>
        /// Processes Toggle operation on a togglable <see cref="Control"/> like <see cref="ToggleButton"/> or <see cref="CheckBox"/> which has the given id.
        /// </summary>
        /// <param name="controlId"></param>
        public void ProcessToggle(string controlId)
        {
            AutomationElement element = GetElementById(controlId);

            Object objPattern;
            TogglePattern togPattern;
            if (element.TryGetCurrentPattern(TogglePattern.Pattern, out objPattern))
            {
                togPattern = objPattern as TogglePattern;
                togPattern.Toggle();
                Thread.Sleep(1500);
            }
        }

        #endregion public

        #endregion methods

    }
}
